接下來三天,我們介紹Python三種常用的protocols。
根據Python docs的說明,sequence
是一個實作有__len__
及__getitem__
,且能以整數作為index
取值的iterable
。
Sequence protocol有時候也被稱作old-style iteration protocol。
__len__
__len__
必須回傳整數值。它除了讓我們可以使用len(obj)
的語法來得知sequence
的長度外,也會在沒有實作某些dunder method
時,和__getitem__
聯手,提供相同的功能。
__getitem__
__getitem__
是一個有趣的dunder method
,它讓我們能夠使用[]
來取值,像list
因此可以使用整數
或slice
作為index
來取值,而dict
因此可以使用hashable
的obj
作為key
來取值。
__iter__
的備案當__getitem__
符合下列條件時,sequence
可以在不實作__iter__
的情況下,視為iterable
。
index
會從0開始呼叫,當index
在可取值範圍內回傳其值。當超出範圍時,raise IndexError
。
這個描述非常類似list
,而的確我們也常使用list
作為sequence
所真正包含的容器。
__contains__
的備案__contains__
是Python用來處理membership test的dunder method
。當使用in obj
,而obj
沒有實作__contains__
時,會改使用__iter__
。如果再沒有實作__iter__
的話,會改使用__getitem__
。
__reversed__
的備案__reversed__
一般被Python的built-in
reversed
所呼叫。當使用reversed(obj)
,而obj
沒有實作__reversed__
時,會使用__getitem___
和__len__
來達成reversed
的功能。
# 01
中MySeq
是一個實作有__getitem__
與__len__
的class
,所以my_seq
可以使用[]
來取值。
# 01
class MySeq:
def __init__(self, iterable):
self._list = list(iterable)
def __len__(self):
print('__len__ called')
return len(self._list)
def __getitem__(self, value):
print(f'__getitem__ called, {value=}')
try:
return self._list[value]
except Exception as e:
print(type(e), e)
raise
if __name__ == '__main__':
my_seq = MySeq(range(3))
print('*****test []*****')
print(f'{my_seq[0]=}') # 0
*****test []*****
__getitem__ called, value=0
my_seq[0]=0
由於我們將__getitem__
內取值的任務delegate
給list
,所以符合前面「__iter__
的備案」所述的條件,因此Python會將my_seq
視為iterable
。
# 01
...
if __name__ == '__main__':
...
print('*****test is an iterable*****')
for item in my_seq:
pass
*****test is an iterable*****
__getitem__ called, value=0
__getitem__ called, value=1
__getitem__ called, value=2
__getitem__ called, value=3
<class 'IndexError'> list index out of rang
我們也可以觀察,當index=3
時,因為超過了my_seq
能接收的範圍,self._list
會報錯,而其錯誤型態的確為IndexError
。
# 01
...
if __name__ == '__main__':
...
print('*****test in operator*****')
print(f'{2 in my_seq=}')
*****test in operator*****
__getitem__ called, value=0
__getitem__ called, value=1
__getitem__ called, value=2
2 in my_seq=True
由於my_seq
沒有實作__contains__
與__iter__
,所以Python會依靠__getitem__
逐個取值,來比對2有沒有在my_seq
中。
# 01
...
if __name__ == '__main__':
...
print('*****test is reversible*****')
for i in reversed(my_seq):
pass
*****test is reversible*****
__len__ called
__getitem__ called, value=2
__getitem__ called, value=1
__getitem__ called, value=0
由於my_seq
沒有實作__reversed__
,所以Python會同時使用__getitem__
及__len__
來達成reversed
的功能。
Python的collections.abc中有Sequence
及MutableSequence
兩種abstract base class
,方便我們繼承使用。文件中有說明我們必須實作哪些dunder method
,而根據這些dunder method
,Python將能自動提供其它額外的method
可以使用。
如果繼承Sequence
的話,只需要實作__getitem__
與__len__
,就能額外獲得__contains__
、__iter__
、__reversed__
、index
與count
。
如果繼承MutableSequence
的話,只需要實作__getitem__
、 __setitem__
、__delitem__
、__len__
與insert
,就能獲得繼承Sequence
額外獲得的method
加上append
、reverse
、extend
、pop
、remove
與 __iadd__
。
雖然只實作__getitem__
與__len__
,就可以作為很多dunder method
的備案,但是依靠__getitem__
逐個取值的效率是比較差的。所以如果可能的話,我們會建議針對各種dunder method
實作比較有效率的邏輯。